﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.ComponentModel.Design.Serialization;
using System.Drawing;
using System.Drawing.Design;
using System.Windows.Forms.Design;

namespace System.Windows.Forms.UITests;

public class DesignBehaviorsTests : ControlTestBase
{
    public DesignBehaviorsTests(ITestOutputHelper testOutputHelper)
        : base(testOutputHelper)
    {
    }

    [WinFormsFact]
    public async Task DesignBehaviorsTests_can_DragDrop_ToolboxItem()
    {
        // Regression test for https://github.com/dotnet/winforms/issues/6919, and it verifies that we can successfully
        // drag and drop a toolbox item from a toolbox on to a designer surface.

        Application.ThreadException += (s, e) =>
        {
            // This will preserve the full stack, which otherwise gets replaced
            throw new InvalidOperationException(e.Exception.Message, e.Exception);
        };

        await RunSingleControlTestAsync<TreeView>(async (form, treeView) =>
        {
            // This will indicate when the designer surface has loaded
            SemaphoreSlim loadSignal = new(0);
            // This will indicate when the drag and drop operation has completed
            SemaphoreSlim dndSignal = new(0);

            form.Size = new(600, 550);
            treeView.Dock = DockStyle.Left;
            treeView.Width = 100;

            // Create a toolbox item and assign it to the node
            // When we drag the node to the design surface, this will go through the toolbox service API
            ToolboxItem toolboxItem = new()
            {
                TypeName = typeof(SampleControl).FullName,
                DisplayName = nameof(SampleControl),
                AssemblyName = typeof(SampleControl).Assembly.GetName()
            };

            TreeNode? node = treeView.Nodes.Add("root");
            node.Tag = toolboxItem;

            // Create the designer surface and all required plumbing
            SampleDesignerLoader designerLoader = new();
            ServiceContainer serviceContainer = new();
            DesignSurface designSurface = new(serviceContainer);

            var designerHost = (IDesignerHost)designSurface.GetService(typeof(IDesignerHost))!;
            serviceContainer.RemoveService(typeof(IToolboxService), false);
            serviceContainer.AddService(typeof(IToolboxService), new SampleToolboxService(designerHost));

            if (designerHost.GetService(typeof(IComponentChangeService)) is IComponentChangeService componentChangeService)
            {
                componentChangeService.ComponentAdded += (s, e) =>
                {
                    if (e.Component is Form form)
                    {
                        form.Text = @"RootForm";
                        form.Size = new Size(400, 400);
                    }
                    else
                    {
                        ((Control)e.Component!).Size = new Size(50, 50);
                    }
                };
            }

            designSurface.Loaded += (s, e) =>
            {
                Control rootView = (Control)designSurface.View;
                rootView.Dock = DockStyle.Fill;
                form.Controls.Add(rootView);
                rootView.BringToFront();

                // Indicate that we have finished loading the designer surface
                loadSignal.Release();
            };

            treeView.ItemDrag += treeView_ItemDrag;

            designSurface.BeginLoad(designerLoader);

            // Wait for the designer surface to load
            await loadSignal.WaitAsync(millisecondsTimeout: 3_000);

            // Initiate the drag and drop and wait for the signal that it finished
            var dndStartCoordinates = treeView.PointToScreen(node.Bounds.Location);
            await InitiateDrangDropAsync(form, dndStartCoordinates, (Control)designSurface.View);
            await dndSignal.WaitAsync(millisecondsTimeout: 3_000);

            return;

            void treeView_ItemDrag(object? sender, ItemDragEventArgs e)
            {
                if (designerHost?.GetService(typeof(IToolboxService)) is not IToolboxService toolboxService ||
                    e.Item is not TreeNode node ||
                    node.Tag is not ToolboxItem toolboxItem)
                {
                    return;
                }

                var dataObject = toolboxService.SerializeToolboxItem(toolboxItem) as DataObject;

                var effects = node.TreeView!.DoDragDrop(dataObject!, DragDropEffects.Copy);
            }

            async Task InitiateDrangDropAsync(Form form, Point startCoordinates, Control rootView)
            {
                var virtualPointStart = ToVirtualPoint(startCoordinates);
                startCoordinates.Offset(110, 50);
                var virtualPointEnd = ToVirtualPoint(startCoordinates);

                await InputSimulator.SendAsync(
                    form,
                    inputSimulator => inputSimulator.Mouse.MoveMouseTo(virtualPointStart.X + 6, virtualPointStart.Y + 6)
                                                          .LeftButtonDown()
                                                          .MoveMouseTo(virtualPointEnd.X, virtualPointEnd.Y)
                                                          .LeftButtonUp());

                dndSignal.Release();
            }
        });
    }

#pragma warning disable CS8603 // Possible null reference return.
    internal class SampleToolboxService : IToolboxService
    {
        private readonly IDesignerHost _designerHost;

        public SampleToolboxService(IDesignerHost designerHost)
        {
            _designerHost = designerHost;
        }

        public void AddCreator(ToolboxItemCreatorCallback creator, string format)
        {
            // this method intentionally keep empty
        }

        public void AddCreator(ToolboxItemCreatorCallback creator, string format, IDesignerHost host)
        {
            // this method intentionally keep empty
        }

        public void AddLinkedToolboxItem(ToolboxItem toolboxItem, IDesignerHost host)
        {
            // this method intentionally keep empty
        }

        public void AddLinkedToolboxItem(ToolboxItem toolboxItem, string category, IDesignerHost host)
        {
            // this method intentionally keep empty
        }

        public void AddToolboxItem(ToolboxItem toolboxItem)
        {
            // this method intentionally keep empty
        }

        public void AddToolboxItem(ToolboxItem toolboxItem, string category)
        {
            // this method intentionally keep empty
        }

        public ToolboxItem DeserializeToolboxItem(object serializedObject)
        {
            return DeserializeToolboxItem(serializedObject, _designerHost);
        }

        public ToolboxItem DeserializeToolboxItem(object serializedObject, IDesignerHost? host)
        {
#pragma warning disable WFDEV005 // Type or member is obsolete
            ToolboxItem? item = ((DataObject)serializedObject)?.GetData(typeof(ToolboxItem)) as ToolboxItem;
#pragma warning restore WFDEV005
            return item!;
        }

        public ToolboxItem GetSelectedToolboxItem()
        {
            return GetSelectedToolboxItem(null!);
        }

        public ToolboxItem GetSelectedToolboxItem(IDesignerHost host)
        {
            return null;
        }

        public ToolboxItemCollection GetToolboxItems()
        {
            return null;
        }

        public ToolboxItemCollection GetToolboxItems(IDesignerHost host)
        {
            return null;
        }

        public ToolboxItemCollection GetToolboxItems(string category)
        {
            return null;
        }

        public ToolboxItemCollection GetToolboxItems(string category, IDesignerHost host)
        {
            return null;
        }

        public bool IsSupported(object serializedObject, IDesignerHost host)
        {
            return true;
        }

        public bool IsSupported(object serializedObject, ICollection filterAttributes)
        {
            return true;
        }

        public bool IsToolboxItem(object serializedObject)
        {
            return true;
        }

        public bool IsToolboxItem(object serializedObject, IDesignerHost host)
        {
            return true;
        }

        public void Refresh()
        {
            throw new NotImplementedException();
        }

        public void RemoveCreator(string format)
        {
            // this method intentionally keep empty
        }

        public void RemoveCreator(string format, IDesignerHost host)
        {
            // this method intentionally keep empty
        }

        public void RemoveToolboxItem(ToolboxItem toolboxItem)
        {
            // this method intentionally keep empty
        }

        public void RemoveToolboxItem(ToolboxItem toolboxItem, string category)
        {
            // this method intentionally keep empty
        }

        public void SelectedToolboxItemUsed()
        {
            // this method intentionally keep empty
        }

        public object SerializeToolboxItem(ToolboxItem toolboxItem)
        {
            return new DataObject(toolboxItem);
        }

        public bool SetCursor()
        {
            return false;
        }

        public void SetSelectedToolboxItem(ToolboxItem toolboxItem)
        {
            // this method intentionally keep empty
        }

        public CategoryNameCollection CategoryNames => null;

        public string SelectedCategory
        {
            get => null;
            set { }
        }
    }

    internal class SampleDesignerLoader : BasicDesignerLoader
    {
        protected override void PerformFlush(IDesignerSerializationManager serializationManager)
        {
            throw new NotImplementedException();
        }

        protected override void PerformLoad(IDesignerSerializationManager serializationManager)
        {
            if (LoaderHost is null)
                return;

            ArrayList errors = [];

            LoaderHost.CreateComponent(typeof(Form));

            LoaderHost.EndLoad(nameof(Form), true, errors);
        }
    }

    [Designer(typeof(SampleControlDesigner))]
    public class SampleControl : Control
    {
    }

    internal class SampleControlDesigner : ControlDesigner
    {
        protected override void OnPaintAdornments(PaintEventArgs pe)
        {
            base.OnPaintAdornments(pe);

            pe.Graphics.DrawString($"Design time \n{Control.Site?.Name} !", Control.Font, SystemBrushes.WindowText, new PointF(12, 12));
        }
    }
}
#pragma warning restore CS8603

